上次介紹插槽時有看到v-for
渲染的案例,v-for
是Vue官方提供的列表渲染(List rendering)指令之
一,可以用來遍歷陣列或對象,在模板中生成一组相同結構的元素,使用起來滿簡單,但是實務常常看到一些的誤區使用和陷阱。在撰寫文章資源的同時,也發現到一些受控和非受控元件
的觀念沒有很清楚,希望也能夠一併統合觀念,打破一些不了解模糊的部分。
就地更新策略(in place patch)
非受控元素觀念認識
(uncontrolled component)-認識樣板ref
不綁定key值的缺點
index
作為 key
的缺點就地更新(in-place patch
)指的是v-for 列表渲染
的元素列表中,Vue 會選擇不重新創建或重新排列 DOM 元素的情况下,只更新現有 DOM 元素的綁定的資料内容,減少不必要的 DOM 操作移動進而提高效率
。
就地更新,如果只有預設綁定的響應式系統資料變動,DOM本身順序結構不動的話,只替換資料。
對於一個陣列物物件列表有順序變更(ex: 排序變化),Vue 會盡可能記取元素對應位置,重複使用原本的 DOM 元素,而不是移除舊元素並建立新元素再渲染資料。
這意味著Vue預設只會將新資料套用到原本的 DOM 元素上,而不會調整 DOM 中元素的順序,也不會連動由DOM元素本身控制的狀態,特別是非受控組控制的資料流的狀態(uncontrolled component)
。
input元素
有一些聚焦(focous)、滾軸功能(scroll behaviour)
,這些由瀏覽器Web API提供的功能對原生 DOM 元素的即時操作,本身並不在Vue所提供的API下。為了達成操作Vue本身沒有瀏覽器API方法,樣板ref
是 Vue template樣板元件上的一個特殊屬性,讓開發者可以樣板上直接獲取到 DOM 元素或子組件的實例引用。但從本質上來說,ref 本身並不是直接操作網頁的 DOM 元素
,而是 Vue 提供的對應 DOM 或組件實例的參考,具體來說比較是一種映射關係
,這樣我們可以在響應式系統下更方便地操作。
一般來說官方會見是使用生命週期鉤子onMounted
,它的執行點會在瀏覽器渲染完成後,因此可用來取得Vue幫我們映射好的網頁dom元素和vue本身的元件實例,藉由console.log打印你觀察除了發現DOM元素和對應瀏覽器API外,也可能是一個子元件的實例在裡面。
<template>
<div>
<!-- ref 會映射到這個 input 元素 -->
<input ref="inputElement" type="text" />
<child-component ref="childComponent" />
<button @click="focusInput">Focus Input</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const inputElement = ref(null);
const childComponent = ref(null);
function focusInput() {
// 在這裡,我們使用映射來訪問 input 元素並調用它的焦點方法
inputElement.value.focus();
}
onMounted(() => {
console.log(inputElement.value); // 這裡的 value 是 DOM 元素
console.log(childComponent.value); // 這裡的 value 是子組件的實例
});
</script>
template ref
確實可以用來取得 DOM 元素的直接引用,但它本身並不是響應式的,也就是說,ref
取得的 DOM 元素引用並不會自動受到 Vue 的響應式系統監控。
換句話說,當你使用 template ref
來引用某個 DOM 元素後,雖然你可以對這個 DOM 元素使用一些瀏覽器API進行資料操作,但這些操作的變化不會觸發 Vue 的資料流或響應式更新,像是下方的案例可以玩玩看。
總而言之,非受控元件指的是那些不直接依賴 Vue 所定義的數據資料流ref/reactive)来管理資料顯示狀態。
它們的狀態由瀏覽器自己控制,透過使用者的互動來改變,本身不會進到參與Vue數據管理更新中。
像input
輸入內容如果沒有透過v-model
綁定到ref/reactive
中,雖然可以正常打字輸入,但顯示資料是瀏覽器所控制。
使用者可以修改輸入框的值,但 Vue 不會自動參與該值的更新。
<input type="text" value="initial value">
如果你直接使用 checked
來初始化checkbox按鈕的選擇狀態,而不使用 v-model
,這些輸入元素的狀態就會變成非受控組件,由瀏覽器資料流自己控制狀態。
<input type="checkbox" checked>
剛好找的一篇文章是說明Vue列表渲染和非受控元素之間關系,可以進去玩玩看
範例:
你會發現我們初始化checkbox的資料
和狀態
是我們用Vue響應式資料
定義好,將checkbox初始化資料由Vueref先定義掌控,之後我們可以透過點擊checkbox來改變選擇狀態。
但我們製作一個random shuffle
功能,去隨意置換v-for陣列的順序,你會發現checkbox狀態好像就不對的???
仔細觀察會有一種我把某些checbox點掉,按下random shuffle置換陣列順序時,又會回到初始化狀態,順序有點打亂對不上的bug。
<template>
<button @click="shuffleItems">Shuffle Items</button>
<ul>
<li v-for="item in items">
<input type="checkbox" :checked="item.checked">
{{ item.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Apple', checked: true },
{ id: 2, name: 'Banana', checked: false },
{ id: 3, name: 'Cherry', checked: true }
]);
// 随机打亂列表顺序的函数
function shuffleItems() {
items.value = items.value.sort(() => Math.random() - 0.5);
}
</script>
在初次渲染,checkbox狀態由 item.checked
控制,和Vue響應式資料流同步
。
在使用者交互後,如果使用者勾選或取消chekbox,這個狀態實際上的改變是由瀏覽器所控制,不會影響Vue響應式資料流 item.checked 的值
,這裡没有使用 v-model
,會變成非受控的組件狀態
。
執行shuffleItems隨機打亂列表 : 因為没有 key
值,預設 Vue 使用就地更新的策略
,重複利用了原有的 DOM 元素去渲染綁定的響應式資料,但是瀏覽器上DOM元素的cheked狀態,還是原本的瀏覽器自己掌握的資料流
。
checkbox可能会因為Vue資料流有設定item.checked 初始化的狀態,但執行shuffleItems功能後,Vue將響應式資料就地更新(in place patch)至原本順序的DOM元素
上,並不是反映使用者真正互動操作過後 item.checked 的實際值,是一種兩者資料不同步情況。
關係會類似像這張圖一樣:
瀏覽器非受控元素有自己的狀態,但Vue的資料部採取就地更新值接替換,兩者沒有同步在一起
v-model 綁定checkbox 狀態
進入Vue的響應式資料流,變成受控元件(controlled componenet)
綁定v-for key值
,讓Vue DOM更新時知道原本資料和DOM的關係順序。<template>
<button @click="shuffleItems">Shuffle Items</button>
<ul>
<li v-for="item in items">
<input type="checkbox" v-model="item.checked" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
v-model
用在表單(如複選框、文本框等)進行雙向數據綁定
。當使用者在複選框上點選使用 時,v-model 會將 item 的 checked 屬性狀態和複選框的狀態一起绑定。
是在 Vue 中確保 DOM 元素的狀態和綁定的數據一致,並在元素資料的位置或順序(Vue資料流)發生變化時,真正DOM元素能夠依據key值正確地移動和更新
,這樣複選框的狀態就會隨著Vue資料流一起變動。
把原本非受控DOM的狀態,加入key 讓Vue之知道說要v-for列表的更新跟著綁定的Vue資料流走:
儘管Vue允許index
可以作為v-for列表渲染時的 key
使用,但它可能在某些情況下導致意想不到的問題。
順序結構改變,整張列表都要一律重新渲染
,即便某些資料真的都沒異動。使用 index
作為 v-for 列表選渲染的 key時,一旦列表的長度發生變化(比如增加或移除項目), Vue 會重新渲染整張列表資料,即便可能只是在資料的頭或尾巴補一筆新資料,可以打開範例使用瀏覽器devtool檢查是否會造成全部列表重新更新渲染一次。
使用 item 的唯一id值作為key
後,當你在陣列頭部插入或刪除元素時,Vue 只會重新渲染實際發生變化的部分,而不會重新渲染整個列表。這將會減少 DOM 操作的次數,優化頁面的渲染效能,尤其是在表單資料陣列較大時效果更為顯著。
可以玩玩看有綁定特定id
的和index
的渲染差別~
<script setup>
import { ref } from 'vue'
const array = ref(['a','b','c','d'])
const obj = ref([
{
id: 1,
val:'a'
},{
id: 2,
val:'b'
},{
id: 3,
val:'c'
},{
id: 4,
val:'d'
}])
const insert = () => {
array.value.splice(0, 0, 'f')
}
const remove = () => {
array.value.splice(0, 1)
}
const insertObj = ()=> {
const add = {
id: self.crypto.randomUUID(),
val: 'dd' + self.crypto.randomUUID()
}
obj.value.push(add)
}
const removeObj = () => {
obj.value.splice(0, 1)
}
</script>
<template>
<button @click="insert">add f</button>
<button @click="remove">remove</button>
<div v-for="(item,index) in array" :key="index">{{item}}</div>
<button @click="insertObj">add obj</button>
<button @click="removeObj">remove obj</button>
<div v-for="(item) in obj" :key="item.id">{{item}}</div>
</template>
在 Vue 中 v-model
可以使資料數據流完全受控於響應式系統資料中,確保表單輸入和數據模型的同步,較不會出現受控和非受控元素資料流分離情況產生
。
但光使用 v-model還不足以完全避免 UI 和數據狀態的脫離狀況。為了確保一致性,在使用 v-for 渲染列表時,應避免使用 index 作為 key
。
因為 index 對應的資料可能是不固定的,而且index 並不是專一性的unique key
,當列表發生插入、刪除或排序等操作時,index 會變化進而導致 Vue 無法綁定正確的資料,使用上key 應該選擇一個唯一且穩定的標識符(如 id)。
https://medium.com/@seed45699/vue-%E7%9A%84-key-%E5%80%BC%E5%88%A5%E5%86%8D%E7%B6%81-index-%E5%95%A6-5180fa71021
https://vuejs.org/guide/essentials/list.html#displaying-filtered-sorted-results
https://medium.com/@mkidsc1603/vue3-v-for-key-bind-index-issue-%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90-7cb4826d6c10
https://deepsource.com/blog/key-attribute-vue-js
https://vueschool.io/articles/vuejs-tutorials/tips-and-gotchas-for-using-key-with-v-for-in-vue-js-3/
https://dev.to/katelynjewel/controlled-vs-uncontrolled-components-44e0